//==============================================================================
//
//                 Arguments - the argument parser class
//
//               Copyright (C) 2018  Dick van Oudheusden
//  
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free
// Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//
//==============================================================================

#include <iostream>
#include <sstream>
#include <cstring>

#include "Arguments.h"

namespace arg
{
  // Switch argument implementation
  Argument::Argument(Arguments         &arguments,
                     bool               optional,
                     char               shortOption,
                     const std::string &longOption,
                     const std::string &help) :
    _optional(optional),
    _withValue(false),
    _active(false),
    _shortOption(shortOption),
    _longOption(longOption),
    _valueInfo(""),
    _value(""),
    _helpInfo(help)
  {
    arguments.add(*this);
  }

  // Value argument implementation
  Argument::Argument(Arguments         &arguments,
                     bool               optional,
                     char               shortOption,
                     const std::string &longOption,
                     const std::string &valueInfo,
                     const std::string &helpInfo,
                     const std::string &initial) :
    _optional(optional),
    _withValue(true),
    _active(false),
    _shortOption(shortOption),
    _longOption(longOption),
    _valueInfo(valueInfo),
    _helpInfo(helpInfo),
    _value(initial)
  {
    arguments.add(*this);
  }

  Argument::~Argument()
  {

  }

  int Argument::optionWidth() const
  {
    return _longOption.length() + (_valueInfo.empty() ? 0 : _valueInfo.length() + 1);
  }

  void Argument::print(int column) const
  {
    Argument::print(_shortOption, _longOption, _valueInfo, _helpInfo, column);
  }

  void Argument::print(char shortOption, const std::string &longOption, const std::string &valueInfo, const std::string &helpInfo, int column)
  {
    std::stringstream str;

    str << "   ";

    if (shortOption != '\0')
    {
      str << '-' << shortOption;
    }
    else
    {
      str << "  ";
    }

    if ((shortOption != '\0') && (!longOption.empty()))
    {
      str << ", ";
    }
    else
    {
      str << "  ";
    }

    if (!longOption.empty())
    {
      str << "--" << longOption;

      if (!valueInfo.empty())
      {
        str << "=" << valueInfo;
      }
    }

    int extra = column - str.str().size();

    str << std::string(extra, ' ') << helpInfo;

    std::cout << str.str() << std::endl;
  }


  // Arguments implementation
  Arguments::Arguments(const std::string &usage, const std::string &version, const std::string &help) :
    _usage(usage),
    _version(version),
    _help(help)
  {

  }

  Arguments::~Arguments()
  {

  }

  void Arguments::add(Argument &argument)
  {
    _arguments.push_back(&argument);
  }

  bool Arguments::parse(int argc, char *argv[], std::vector<std::string> &unprocessed)
  {
    // Reset in-use
    for (auto argument = _arguments.begin(); argument != _arguments.end(); ++argument)
    {
      (*argument)->active(false);
    }

    // Parse the arguments
    int arg = 1;
    while (arg < argc)
    {
      size_t arglen = strlen(argv[arg]);

      if ((arglen >= 2) && (argv[arg][0] == '-') && (argv[arg][1] == '-'))
      {
        if (arglen == 2) break; // special: --, stop parsing

        if (!parseLongOption(argc, argv, arg)) return false;
      }
      else if ((arglen >= 1) && (argv[arg][0] == '-'))
      {
        if (!parseShortOption(argc, argv, arg)) return false;
      }
      else
      {
        unprocessed.push_back(argv[arg]);
      }

      arg++;
    }

    // Save the arguments after --
    for (arg++; arg < argc; arg++)
    {
      unprocessed.push_back(argv[arg]);
    }

    // Check the required arguments
    for (auto argument = _arguments.begin(); argument != _arguments.end(); ++argument)
    {
      if (!checkRequiredArgument(*argument)) return false;
    }

    return true;
  }

  bool Arguments::parseShortOption(int argc, char *argv[], int &arg)
  {
    if (argv[arg][1] == '\0')
    {
      std::cerr << "Invalid argument: " << argv[arg] << std::endl;
      return false;
    }

    if (argv[arg][1] == '?')
    {
      printHelp();
      return false;
    }

    size_t i = 1;
    while (argv[arg][i] != '\0')
    {
      auto argument = _arguments.begin();
      for (; argument != _arguments.end(); ++argument)
      {
        char shortOption = (*argument)->shortOption();

        if ((shortOption != '\0') && (shortOption == argv[arg][i]))
        {
          (*argument)->active(true);

          if ((*argument)->withValue())
          {
            if (argv[arg][i+1] != '\0')
            {
              (*argument)->value(&argv[arg][i+1]);
              return true;
            }
            else if ((arg+1) < argc)
            {
              (*argument)->value(argv[++arg]);
              return true;
            }
            else
            {
              std::cerr << "Missing value for argument: -" << argv[arg][i] << std::endl;
              return false;
            }
          }

          break;
        }
      }

      if (argument == _arguments.end())
      {
        std::cerr << "Unknown argument: -" << argv[arg][i] << std::endl;
        return false;
      }

      i++;
    }

    return true;
  }

  bool Arguments::parseLongOption(int argc, char *argv[], int arg)
  {
    std::string option(argv[arg] + 2); // skip --
    std::string value;

    if (option == "help")
    {
      printHelp();
      return false;
    }

    if (option == "version")
    {
      std::cout << _version << std::endl;
      return false;
    }

    // Split option and value
    std::size_t assign = option.find_first_of("=:");
    if (assign != std::string::npos)
    {
      value  = option.substr(assign+1);
      option = option.substr(0, assign);
    }

    auto argument = _arguments.begin();
    for (; argument != _arguments.end(); ++argument)
    {
      if (option == (*argument)->longOption())
      {
        (*argument)->active(true);

        if ((*argument)->withValue())
        {
          if (!value.empty())
          {
            (*argument)->value(value);
          }
          else
          {
            std::cerr << "Missing value for argument: " << argv[arg] << std::endl;
            return false;
          }
        }

        break;
      }
    }

    if (argument == _arguments.end())
    {
      std::cerr << "Unknown argument: " << argv[arg] << std::endl;
      return false;
    }

    return true;
  }

  bool Arguments::checkRequiredArgument(Argument *argument)
  {
    if ((argument->optional()) || (argument->active())) return true;

    std::cerr << "Missing argument: ";
    if (argument->shortOption() != '\0')
    {
      std::cerr << '-' << argument->shortOption();
    }
    else
    {
      std::cerr << "--" << argument->longOption();
    }
    std::cerr << std::endl;

    return false;
  }

  void Arguments::printHelp() const
  {
    std::cout << "Usage: " << _usage << std::endl;

    int column = 7; // "help" + 3 spaces

    for (auto argument = _arguments.begin(); argument != _arguments.end(); ++argument)
    {
      column = std::max(column, (*argument)->optionWidth());
    }

    column += 12; // short option, six spaces and --

    Argument::print('?',  "help",    "", "display this help and exit", column);
    Argument::print('\0', "version", "", "display version and exit",   column);

    for (auto argument = _arguments.begin(); argument != _arguments.end(); ++argument)
    {
      (*argument)->print(column);
    }

    std::cout << std::endl << _help << std::endl;
  }
}
